// import { circuitRelayServer } from 'libp2p/circuit-relay'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import { peerIdFromString } from '@libp2p/peer-id'
import dotenv from 'dotenv'
import Logger from 'logplease'

import { seedHexToSeed, peerIdToDid } from './peer-id.js'
import debug from 'debug'
import pMap from 'p-map'
import { createNode } from './create-node.js';
import { getOfflineInvitations, clearOfflineInvitations, getOfflineChatMessages, clearOfflineChatMessages, saveChatMessage, updateInvitation, deleteMember, updateProfile, getProfile } from './db.js'

const logger = Logger.create('relay')
Logger.setLogLevel('DEBUG')

// debug.enable('*')

dotenv.config()

const swarmKey_dev = '/key/swarm/psk/1.0.0/\n/base16/\n' + '7ba72b5b15148919d364693b0a8cd2c36a09a3345f210c96ccc019e2ace1def6'
const SEED_HEX = '5110e63bf075b89770deb88d89281526d5c858380e76db257a86834bb42d2085'
// 12D3KooWELrypWhpyUef9gtJr5TUne1173fG24QpMrRN62HN4VDk


async function subscribeTopics(node) {
  // subscribe all topics
  node.services.pubsub.addEventListener('subscription-change', (evt) => {
    const msg = evt.detail
    logger.debug('receive subscription', msg)
    for (let sub of msg.subscriptions) {
      let { topic, subscribe } = sub
      // logger.debug('subscribing topic: ', topic)
      // node.services.pubsub.subscribe(topic)
    }
  })
}


let _OnlineMembers = {}

// const HEART_BEAT_TIME = 30000
// const isOnline = (did) => {
//   const peers = node.services.pubsub.getSubscribers(`${did}-channel`)
//   return _OnlineMembers[did] && Date.now() - _OnlineMembers[did].time < 59000
// }
const setHeartBeatTime = (did) => {
  _OnlineMembers[did].time = Date.now()
}

async function syncData(node) {

  const isOnline = (did) => {
    const peers = node.services.pubsub.getSubscribers(`${did}-channel`)
    return peers.length >= 1
  }
  // _AllMembers = await prisma.Members.findMany({});

  // let list = await prisma.ChatMessages.findMany();
  // logger.debug('all chat messages', list)
  // list = await prisma.Invitations.findMany();
  // logger.debug('all invitations', list)

  node.addEventListener('peer:connect', (evt) => {
    let peerId = evt.detail.toString()
    logger.debug('Connected to %s', peerId) // Log connected peer
    let did = peerIdToDid(peerId)
    _OnlineMembers[did] = _OnlineMembers[did] || {}
    _OnlineMembers[did].time = Date.now()
    _OnlineMembers[did].peerId = peerId
    _OnlineMembers[did].connected = true

    logger.debug('subscribe member channel', did)
    node.services.pubsub.subscribe(`${did}-channel`)

    // const topics = node.services.pubsub.getTopics()
    // logger.debug('topics', topics)
    // const peers = node.services.pubsub.getPeers()
    // logger.debug('peers', peers.map(p => p.toString()))
    // let intervalId = setInterval(() => {
    //   node.services.pubsub.subscribe(`${did}-channel`)
    //   const peers = node.services.pubsub.getSubscribers(`${did}-channel`)
    //   if (peers.length > 0) {
    //     clearInterval(intervalId)
    //   }
    // }, 1000)

    let connections = node.components.connectionManager.connections
    logger.debug('connections', connections.size)
    // connections.map.forEach((value, key) => {
    //   logger.debug(key)
    // })
  })

  node.addEventListener('peer:disconnect', (evt) => {
    let peerId = evt.detail.toString()

    let did = peerIdToDid(peerId)
    logger.debug(`Disconnected from libp2p peer ${peerId} ${_OnlineMembers[did] ? _OnlineMembers[did].nickName : ''}`)
    // delete _OnlineMembers[did]
    if (_OnlineMembers[did]) _OnlineMembers[did].connected = false

    logger.debug('unsubscribe member channel', did)
    node.services.pubsub.unsubscribe(`${did}-channel`)
  })

  node.services.pubsub.addEventListener('message', async (evt) => {
    const topic = evt.detail.topic
    const from = evt.detail.from.toString()
    const msg = uint8ArrayToString(evt.detail.data)

    if (topic.endsWith('-channel')) {
      const channelDid = topic.split('-')[0]
      let didProfile = await fetchProfile(channelDid)
      logger.debug(`node2 received: ${msg} from ${from}, ${channelDid}, ${didProfile ? didProfile.nickName : ''}`)

      if (peerIdToDid(from) != channelDid) {
        logger.debug('peerId and did not match, ignore it')
        return
      }
      // node.services.pubsub.direct.add(from)
      // logger.debug('direct peers', node.services.pubsub.direct)

      const body = JSON.parse(msg)

      if (body.type == 'heart-beat') {
        setHeartBeatTime(channelDid)
        node.services.pubsub.publish(`${channelDid}-channel`, uint8ArrayFromString(JSON.stringify({
          type: 'heart-beat-res',
          payload: {}
        })))
      } else if (body.type == 'offline-data-req') {
        await sendOfflineData(node, channelDid)
      } else if (body.type == 'unregister') {
        logger.debug('unregister')
        await deleteMember(channelDid)
      } else if (body.type == 'invitation') { // invitation
        let invitation = body.payload
        let targetDid
        if (invitation.status == 'invite') {
          targetDid = invitation.invitee
        } else if (invitation.status == 'confirmed') {
          targetDid = invitation.invitor
        }
        const online = isOnline(targetDid)
        if (!online) {
          invitation.did = targetDid
          delete invitation.isRead
          logger.debug('member is not online, save invitation')
          await updateInvitation(invitation)
        } else {
          // publish
          node.services.pubsub.publish(`${targetDid}-channel`, uint8ArrayFromString(JSON.stringify({
            type: 'invitation',
            payload: invitation
          })))
        }
      } else if (body.type == 'chat') { // invitation
        let chatMessage = body.payload
        let members = chatMessage.members
        delete chatMessage.members
        for (let mem of members) {
          if (mem == channelDid) continue
          const online = isOnline(mem)
          if (!online) {
            chatMessage.targetDid = mem
            logger.debug('member is not online, save chat message')
            await saveChatMessage(chatMessage)
          } else {
            node.services.pubsub.publish(`${mem}-channel`, new TextEncoder().encode(JSON.stringify(body)));
          }
        }
      } else if (body.type == 'offline-invitations-clear') {
        await clearOfflineInvitations(channelDid)
      } else if (body.type == 'offline-chat-messages-clear') {
        await clearOfflineChatMessages(channelDid)
      } else if (body.type == 'update-profile') {
        let profile = body.payload
        if (profile.did != channelDid) return
        _OnlineMembers[channelDid].nickName = profile.nickName
        _OnlineMembers[channelDid].avatar = profile.avatar
        const updateTime = new Date()
        _OnlineMembers[channelDid].time = updateTime
        profile.time = updateTime
        await updateProfile(profile)
      } else if (body.type == 'fetch-profile') {
        let { targetDid, timestamp } = body.payload
        let dids = Array.isArray(targetDid) ? targetDid : [targetDid]

        let profiles = await pMap(dids, (did) => fetchProfile(did))
        // logger.debug('profiles', profiles)
        let jsonStr = JSON.stringify({
          type: 'fetch-profile-res-' + timestamp,
          payload: Array.isArray(targetDid) ? profiles : profiles[0]
        })
        // logger.debug('profiles json', jsonStr)
        node.services.pubsub.publish(`${channelDid}-channel`, uint8ArrayFromString(jsonStr))
      }

      const ackNeedTypes = ['chat', 'invitation']
      if (ackNeedTypes.includes(body.type)) {
        if (body.ackId) {
          logger.debug('send ack', body.ackId)
          node.services.pubsub.publish(`${channelDid}-channel`, uint8ArrayFromString(JSON.stringify({
            type: 'ack',
            payload: { ackId: body.ackId }
          })))
        }
      }
    }
  })
}

async function fetchProfile(targetDid) {
  let profile
  if (_OnlineMembers[targetDid] && _OnlineMembers[targetDid].nickName) {
    profile = {
      did: targetDid,
      nickName: _OnlineMembers[targetDid].nickName,
      avatar: _OnlineMembers[targetDid].avatar,
    }
  } else {
    try {
      profile = await getProfile(targetDid)
      if (profile) {
        if (!_OnlineMembers[targetDid]) _OnlineMembers[targetDid] = {}
        _OnlineMembers[targetDid].nickName = profile.nickName
        _OnlineMembers[targetDid].avatar = profile.avatar
      }
    } catch (err) {
      logger.debug(`failed to fetch profile for ${targetDid} ${_OnlineMembers[targetDid] ? _OnlineMembers[targetDid].nickName : ''}`, err)
    }
  }

  if (profile) {
    return { ...profile }
  }
  return null
}

async function sendOfflineData(node, did) {
  logger.debug('send offline invitaitons and chat messages')
  // offline invitations
  const list = await getOfflineInvitations(did)
  if (list.length > 0) {
    let body = {
      type: 'offline-invitations',
      payload: list
    }
    node.services.pubsub.publish(`${did}-channel`, uint8ArrayFromString(JSON.stringify(body)))
  }
  // offline chat messages
  const chatMessages = await getOfflineChatMessages(did);
  if (chatMessages.length > 0) {
    let body = {
      type: 'offline-chat-messages',
      payload: chatMessages
    }
    node.services.pubsub.publish(`${did}-channel`, uint8ArrayFromString(JSON.stringify(body)))
  }
}

; (async () => {
  const seed = seedHexToSeed(SEED_HEX)
  const swarmKey = uint8ArrayFromString(swarmKey_dev)
  const node = await createNode({ swarmKey, seed })

  logger.debug(`Node started with id ${node.peerId.toString()}`)
  logger.debug('Listening on:')
  node.getMultiaddrs().forEach((ma) => logger.debug(ma.toString()))

  node.addEventListener('peer:discovery', (evt) => {
    // logger.debug('Discovered: %s', evt.detail.id.toString()) // Log discovered peer
  })

  // disconnect peer if not receive heart beat
  // setInterval(() => {
  //   for (let key in _OnlineMembers) {
  //     let item = _OnlineMembers[key]
  //     if (item.connected) {
  //       const peers = node.services.pubsub.getSubscribers(`${key}-channel`)
  //       // logger.debug(`subscribers for ${key}-channel`, peers)
  //       if (peers.length == 0) {
  //         logger.debug('peer is offline, disconnect it', key, item.peerId, item.nickName)
  //         node.hangUp(peerIdFromString(item.peerId))
  //         item.connected = false
  //       }
  //     }
  //   }
  // }, 1000)

  subscribeTopics(node)
  syncData(node)

})()
